package com.dbflow5.database;

import com.dbflow5.MapUtils;
import com.dbflow5.adapter.CreationAdapter;
import com.dbflow5.config.DBFlowDatabase;
import com.dbflow5.config.FlowLog;
import com.dbflow5.config.NaturalOrderComparator;
import com.dbflow5.migration.Migration;
import ohos.aafwk.ability.DataAbilityRemoteException;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

/**
 * Description: Manages creation, updating, and migrating ac [DBFlowDatabase]. It performs View creations.
 */
public class LocalDatabaseHelper {
    public static final String MIGRATION_PATH = "migrations";

    private final MigrationFileHelper migrationFileHelper;
    DBFlowDatabase databaseDefinition;

    public LocalDatabaseHelper(MigrationFileHelper migrationFileHelper, DBFlowDatabase databaseDefinition){
        this.migrationFileHelper = migrationFileHelper;
        this.databaseDefinition = databaseDefinition;
    }

    // path to migration for the database.
    private String dbMigrationPath(){
        return MIGRATION_PATH + "/" + databaseDefinition.getDatabaseName();
    }

    public void onConfigure(DatabaseWrapper db) {
        checkForeignKeySupport(db);
    }

    public void onCreate(DatabaseWrapper db) {
        // table creations done first to get tables in db.
        executeTableCreations(db);

        // execute any initial migrations when DB is first created.
        // use the databaseversion of the definition, since onupgrade is not called oncreate on a version 0
        // then SQLCipher and Ohos set the DB to that version you choose.
        executeMigrations(db, -1, databaseDefinition.databaseVersion());

        // views reflect current db state.
        executeViewCreations(db);
    }

    public void onUpgrade(DatabaseWrapper db, int oldVersion, int newVersion) {
        // create new tables if not previously created
        executeTableCreations(db);

        // migrations run to get to DB newest version. adjusting any existing tables to new version
        executeMigrations(db, oldVersion, newVersion);

        // views reflect current db state.
        executeViewCreations(db);
    }

    public void onOpen(DatabaseWrapper db) {
    }

    public void onDowngrade(DatabaseWrapper db, int oldVersion, int newVersion) {
    }

    /**
     * If foreign keys are supported, we turn it on the DB specified.
     *
     * @param database database
     */
    protected void checkForeignKeySupport(DatabaseWrapper database) {
        if (databaseDefinition.isForeignKeysSupported()) {
            database.execSQL("PRAGMA foreign_keys=ON;");
            FlowLog.log(FlowLog.Level.I, "Foreign Keys supported. Enabling foreign key features.", null);
        }
    }

    /**
     * executeTableCreations
     *
     * @param database database
     */
    protected void executeTableCreations(DatabaseWrapper database) {
        database.executeTransaction(unused -> {
            databaseDefinition.modelAdapters()
                    .stream()
                    .sequential()
                    .filter(CreationAdapter::createWithDatabase)
                    .forEach(modelAdapter -> {
                        try {
                            modelAdapter.createIfNotExists(database);
                        } catch (SQLiteException e) {
                            FlowLog.logError(e);
                        }
                    });
            return null;
        });
    }

    /**
     * This method executes CREATE TABLE statements as well as CREATE VIEW on the database passed.
     *
     * @param database database
     */
    protected void executeViewCreations(DatabaseWrapper database) {
        database.executeTransaction(unused -> {
            databaseDefinition.modelViewAdapters()
                    .stream()
                    .sequential()
                    .filter(CreationAdapter::createWithDatabase)
                    .forEach(modelAdapter -> {
                        try {
                            modelAdapter.createIfNotExists(database);
                        } catch (SQLiteException e) {
                            FlowLog.logError(e);
                        }
                    });
            return null;
        });
    }

    protected void executeMigrations(DatabaseWrapper db, int oldVersion, int newVersion) {
        // will try migrations file or execute migrations from code
        List<String> files = migrationFileHelper.getListFiles(dbMigrationPath());
        files.sort(new NaturalOrderComparator());

        Map<Integer, List<String>> migrationFileMap = new HashMap<>();
        for (String file : files) {
            try {
                int version = Integer.parseInt(file.replace(".sql", ""));
                List<String> fileList = MapUtils.getOrPut(migrationFileMap, version, new ArrayList<>());
                fileList.add(file);
            } catch (NumberFormatException e) {
                FlowLog.log(FlowLog.Level.W, "Skipping invalidly named file: "+file, e);
            }

        }

        Map<Integer, List<Migration>> migrationMap = databaseDefinition.getMigrations();

        int curVersion = oldVersion + 1;

        try {
            db.beginTransaction();

            // execute migrations in order, migration file first before wrapped migration classes.
            for (int i = curVersion; i<= newVersion; i++){
                List<String> migrationFiles = migrationFileMap.get(i);
                if (migrationFiles != null) {
                    for (String migrationFile : migrationFiles) {
                        executeSqlScript(db, migrationFile);
                        FlowLog.log(FlowLog.Level.I, migrationFile + " executed successfully.");
                    }
                }

                List<Migration> migrationsList = migrationMap.get(i);
                if (migrationsList != null) {
                    for (Migration migration : migrationsList) {
                        // before migration
                        migration.onPreMigrate();

                        // migrate
                        migration.migrate(db);

                        // after migration cleanup
                        migration.onPostMigrate();
                        FlowLog.log(FlowLog.Level.I, migration + " executed successfully.");
                    }
                }
            }
            db.setTransactionSuccessful();
        } catch (DataAbilityRemoteException e) {
            e.printStackTrace();
        } finally {
            db.endTransaction();
        }

    }

    /**
     * Supports multiline sql statements with ended with the standard ";"
     *
     * @param db   The database to run it on
     * @param file the file name in assets/migrations that we read from
     */
    private void executeSqlScript(DatabaseWrapper db, String file) {
        migrationFileHelper.executeMigration(dbMigrationPath() + "/" + file, queryString -> {
            db.execSQL(queryString);
            return null;
        });
    }

}
